<!DOCTYPE html>
<script src="../../../resources/js-test.js"></script>
<script>

window.jsTestIsAsync = true;
var mutations, mutations2, mutationsWithOldValue;
var calls;
var div;

function testBasic() {
    var div;
    var observer;

    function start() {
        debug('Testing basic aspects of attribute observation.');

        mutations = null;
        div = document.createElement('div');
        div.setAttribute('bar', 'foo');

        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        observer.observe(div, { attributes: true, characterData: true });
        div.setAttribute('foo', 'bar');
        div.removeAttribute('bar');
        setTimeout(checkDisconnectAndMutate, 0);
    }

    function checkDisconnectAndMutate() {
        debug('...can attribute changes be observed at all');

        shouldBe('mutations.length', '2');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        shouldBe('mutations[0].attributeNamespace', 'null');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"bar"');
        shouldBe('mutations[1].attributeNamespace', 'null');

        mutations = null;
        observer.disconnect();
        div.setAttribute('foo', 'baz');
        setTimeout(checkNotDeliveredAndMutateMultiple, 0);
    }

    function checkNotDeliveredAndMutateMultiple() {
        debug('...observer.disconnect() should prevent further delivery of mutations.');

        shouldBe('mutations', 'null');
        observer.observe(div, { attributes: true });
        div.setAttribute('foo', 'bat');
        div.setAttribute('bar', 'foo');
        setTimeout(finish);
    }

    function finish() {
        debug('...re-observing after disconnect works with the same observer.');

        shouldBe('mutations.length', '2');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        shouldBe('mutations[0].attributeNamespace', 'null');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"bar"');
        shouldBe('mutations[1].attributeNamespace', 'null');
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testWrongType() {
    var div;
    var observer;

    function start() {
        debug('Testing that observing without specifying "attributes" does not result in hearing about attribute changes.');

        mutations = null;
        div = document.createElement('div');
        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        observer.observe(div, { childList: true, characterData: true });
        div.setAttribute('foo', 'bar');
        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations', 'null');
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testMultipleRegistration() {
    var div;
    var observer;

    function start() {
        debug('Testing that re-observing the same node with the same observer has the effect of resetting the options.');

		calls = 0;
        mutations = null;
        div = document.createElement('div');
        observer = new MutationObserver(function(m) {
            mutations = m;
			calls++;
        });

        observer.observe(div, { attributes: true, characterData: true });
        observer.observe(div, { attributes: true });
        div.setAttribute('foo', 'bar');
        setTimeout(checkDisconnectAndMutate, 0);
    }

    function checkDisconnectAndMutate() {
        shouldBe('calls', '1');
        shouldBe('mutations.length', '1');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        mutations = null;
        observer.observe(div, { attributes: true, characterData: true });
        observer.observe(div, { childList: true });
        div.setAttribute('foo', 'baz');
        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations', 'null');
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testMultipleObservers() {
    var div;
    var observer;
    var observer2;

    function start() {
        debug('Testing that multiple observers can be registered to a given node and both receive mutations.');
        mutations = null;
        div = document.createElement('div');
        observer = new MutationObserver(function(m) {
            mutations = m;
        });
        observer2 = new MutationObserver(function(m) {
            mutations2 = m;
        });
        observer.observe(div, { attributes: true });
        observer2.observe(div, { attributes: true });
        div.setAttribute('foo', 'bar');
        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations.length', '1');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        shouldBe('mutations2.length', '1');
        shouldBe('mutations2[0].type', '"attributes"');
        shouldBe('mutations2[0].attributeName', '"foo"');
        observer.disconnect();
        observer2.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testNamespaceURI() {
    var div;
    var observer;

    function start() {
        debug('Testing that "attributeNamespace" value is delivered properly.');
        mutations = null;
        div = document.createElement('div');
        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        observer.observe(div, { attributes: true, childList: true });
        div.setAttributeNS('http://www.foo.com/bar', 'foo', 'bar');
        setTimeout(finish, 0);    
    }

    function finish() {
        shouldBe('mutations.length', '1');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        shouldBe('mutations[0].attributeNamespace', '"http://www.foo.com/bar"');        
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testPropertyAccess() {
    var img, a;
    var observer;

    function start() {
        debug('Testing that modifications to node properties which delegate to attribute storage deliver mutations.');
        mutations = null;
        img = document.createElement('img');
        a = document.createElement('a');

        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        observer.observe(img, { attributes: true });
        observer.observe(a, { attributes: true });

        img.src = 'baz.png';
        a.href = 'foo.html';

        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations.length', '2');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"src"');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"href"');
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testOrderingWrtDOMSubtreeModified() {
    var div, div2, subDiv;
    var observer;
    var listener;

    function start() {
        debug('Testing mutation records are enqueued for attributes before DOMSubtreeModified is dispatched.');

        mutations = null;
        div = document.body.appendChild(document.createElement('div'));
        div2 = document.body.appendChild(document.createElement('div'));

        subDiv = div.appendChild(document.createElement('div'));

        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        listener = function(e) {
            div2.setAttribute('baz', 'bat');
        }

        div.addEventListener('DOMSubtreeModified', listener);
        observer.observe(subDiv, { attributes: true });
        observer.observe(div2, { attributes: true });

        subDiv.setAttribute('foo', 'bar');

        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations.length', '2');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"baz"');
        div.removeEventListener('DOMSubtreeModified', listener);
        document.body.removeChild(div);
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testOldValue() {
    var div;
    var observer;

    function start() {
        debug('Testing basic oldValue delivery.');
        mutations = null;
        div = document.createElement('div');
        div.setAttribute('bar', 'boo');
        
        observer = new MutationObserver(function(mutations) {
            window.mutations = mutations;
        });
        observer.observe(div, { attributes: true, attributeOldValue: true });
        div.setAttribute('foo', 'bar');
        div.setAttribute('foo', 'baz');
        div.removeAttribute('bar');
        div.removeAttribute('non-existant');
        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations.length', '3');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        shouldBe('mutations[0].oldValue', 'null');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"foo"');
        shouldBe('mutations[1].oldValue', '"bar"');
        shouldBe('mutations[2].type', '"attributes"');
        shouldBe('mutations[2].attributeName', '"bar"');
        shouldBe('mutations[2].oldValue', '"boo"');
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testOldValueAsRequested() {
    var div;
    var observerWithOldValue;
    var observer;

    function start() {
        debug('Testing that oldValue is delivered as requested (or not).');
        mutationsWithOldValue = null;
        mutations = null;
        div = document.createElement('div');
        div.setAttribute('foo', 'bar');
        observerWithOldValue = new MutationObserver(function(mutations) {
            window.mutationsWithOldValue = mutations;
        });
        observer = new MutationObserver(function(mutations) {
            window.mutations = mutations;
        });
        observerWithOldValue.observe(div, { attributes: true, attributeOldValue: true });
        observer.observe(div, { attributes: true });
        div.setAttribute('foo', 'baz');
        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutationsWithOldValue.length', '1');
        shouldBe('mutationsWithOldValue[0].type', '"attributes"');
        shouldBe('mutationsWithOldValue[0].attributeName', '"foo"');
        shouldBe('mutationsWithOldValue[0].oldValue', '"bar"');
        shouldBe('mutations.length', '1');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        shouldBe('mutations[0].oldValue', 'null');
        observerWithOldValue.disconnect();
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testOldValueUnionMultipleObservations() {
    var div;
    var span;
    var observer;

    function start() {
        debug('An observer with multiple observations will get attributeOldValue if any entries request it.');
        mutations = null;
        div = document.createElement('div');
        span = div.appendChild(document.createElement('span'));
        span.setAttribute('foo', 'bar');
        observer = new MutationObserver(function(mutations) {
            window.mutations = mutations;
        });
        observer.observe(div, { attributes: true, attributeOldValue: true, subtree: true });
        observer.observe(span, { attributes: true });
        span.setAttribute('foo', 'baz');
        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations.length', '1');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        shouldBe('mutations[0].oldValue', '"bar"');
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testIDLAttribute() {
    var div;
    var observer;

    function start() {
        debug('Testing setting an attribute via reflected IDL attribute.');
        mutations = null;
        div = document.createElement('div');
        observer = new MutationObserver(function(mutations) {
            window.mutations = mutations;
        });
        observer.observe(div, { attributes: true, attributeOldValue: true });
        div.id = 'foo';
        div.id = 'bar';
        div.id = null;
        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations.length', '3');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"id"');
        shouldBe('mutations[0].oldValue', 'null');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"id"');
        shouldBe('mutations[1].oldValue', '"foo"');
        shouldBe('mutations[2].type', '"attributes"');
        shouldBe('mutations[2].attributeName', '"id"');
        shouldBe('mutations[2].oldValue', '"bar"');
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testAttributeFilter() {
    var div, path;
    var observer;

    function start() {
        debug('Testing that attributeFilter works as expected and observes case with HTML elements.');

        mutations = null;
        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        div = document.createElement('div');
        observer.observe(div, { attributes: true, attributeFilter: ['foo', 'bar', 'booM'] });
        div.setAttribute('foo', 'foo');
        div.setAttribute('bar', 'bar');
        div.setAttribute('baz', 'baz');
        div.setAttribute('BOOm', 'boom');

        setTimeout(finish, 0);
    }

    function finish() {
        debug('...only foo and bar should be received.');

        shouldBe('mutations.length', '2');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        shouldBe('mutations[0].attributeNamespace', 'null');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"bar"');
        shouldBe('mutations[1].attributeNamespace', 'null');
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testAttributeFilterSubtree() {
    var div, div2, div3;
    var observer;

    function start() {
        debug('Testing the behavior of attributeFilter when the same observer observes at multiple nodes in a subtree with different filter options.');

        mutations = null;
        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        div = document.createElement('div');
        div2 = div.appendChild(document.createElement('div'));
        div3 = div2.appendChild(document.createElement('div'));

        observer.observe(div, { attributes: true, subtree: true, attributeFilter: ['foo', 'bar'] });
        observer.observe(div2, { attributes: true, subtree: true, attributeFilter: ['bar', 'bat'] });

        div3.setAttribute('foo', 'foo');
        div3.setAttribute('bar', 'bar');
        div3.setAttribute('bat', 'bat');
        div3.setAttribute('baz', 'baz');

        setTimeout(checkAndObserveAll, 0);
    }

    function checkAndObserveAll() {
        debug('...only foo, bar & bat should be received.');

        shouldBe('mutations.length', '3');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"foo"');
        shouldBe('mutations[0].attributeNamespace', 'null');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"bar"');
        shouldBe('mutations[1].attributeNamespace', 'null');
        shouldBe('mutations[2].type', '"attributes"');
        shouldBe('mutations[2].attributeName', '"bat"');
        shouldBe('mutations[2].attributeNamespace', 'null');

        observer.observe(div2, { attributes: true, subtree: true });
        div3.setAttribute('bar', 'bar');
        div3.setAttribute('bat', 'bat');
        div3.setAttribute('baz', 'baz');

        setTimeout(finish, 0);
    }

    function finish() {
        debug('...bar, bat & baz should all be received.');

        shouldBe('mutations.length', '3');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"bar"');
        shouldBe('mutations[0].attributeNamespace', 'null');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"bat"');
        shouldBe('mutations[1].attributeNamespace', 'null');
        shouldBe('mutations[2].type', '"attributes"');
        shouldBe('mutations[2].attributeName', '"baz"');
        shouldBe('mutations[2].attributeNamespace', 'null');

        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testAttributeFilterNonHTMLElement() {
    var path;
    var observer;

    function start() {
        debug('Testing that setting an attributeFilter filters out namespaced attributes.');

        mutations = null;
        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        observer.observe(path, { attributes: true, attributeFilter: ['pathLength'] });
        path.setAttributeNS('http://www.w3.org/2000/svg', 'pathLength', '200');

        setTimeout(finish, 0);
    }

    function finish() {
        debug('...pathLength should not be received.');

        shouldBeNull('mutations');
        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testAttributeFilterNonHTMLDocument() {
    var svgDoc, div, path;
    var observer;

    function start() {
        debug('Testing that attributeFilter respects case with non-HTML elements.');

        svgDoc = document.implementation.createDocument('http://www.w3.org/2000/svg', 'svg');
        mutations = null;
        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        div = svgDoc.createElement('div');
        observer.observe(div, { attributes: true, attributeFilter: ['ID', 'id', 'booM'] });
        div.setAttribute('ID', 'ID');
        div.setAttribute('id', 'id');
        div.setAttribute('baz', 'baz');
        div.setAttribute('booM', 'boom');
        div.setAttribute('BOOm', 'boom');

        setTimeout(finish, 0);
    }

    function finish() {
        debug('...only ID, id, booM should be received.');

        shouldBe('mutations.length', '3');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"ID"');
        shouldBe('mutations[0].attributeNamespace', 'null');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"id"');
        shouldBe('mutations[1].attributeNamespace', 'null');
        shouldBe('mutations[2].type', '"attributes"');
        shouldBe('mutations[2].attributeName', '"booM"');
        shouldBe('mutations[2].attributeNamespace', 'null');

        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testStyleAttributePropertyAccess() {
    var div, path;
    var observer;

    function start() {
        debug('Testing that modifying an elements style property dispatches Mutation Records.');

        mutations = null;
        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        div = document.createElement('div');
        div.setAttribute('style', 'color: yellow; width: 100px;');
        observer.observe(div, { attributes: true });
        div.style.color = 'red';
        div.style.width = '200px';
        div.style.color = 'blue';

        setTimeout(checkAndContinue, 0);
    }

    function checkAndContinue() {
        shouldBe('mutations.length', '3');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"style"');
        shouldBe('mutations[0].oldValue', 'null');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"style"');
        shouldBe('mutations[1].oldValue', 'null');
        shouldBe('mutations[2].type', '"attributes"');
        shouldBe('mutations[2].attributeName', '"style"');
        shouldBe('mutations[2].oldValue', 'null');

        mutations = null;
        div.getAttribute('style');
        setTimeout(finish, 0);
    }

    function finish() {
        debug('...mutation record created.');

        shouldBe('mutations', 'null');

        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testStyleAttributePropertyAccessOldValue() {
    var div, path;
    var observer;

    function start() {
        debug('Testing that modifying an elements style property dispatches Mutation Records with correct oldValues.');

        mutations = null;
        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        div = document.createElement('div');
        div.setAttribute('style', 'color: yellow; width: 100px;');
        observer.observe(div, { attributes: true, attributeOldValue: true });
        div.style.color = 'red';
        div.style.width = '200px';
        div.style.color = 'blue';

        setTimeout(checkAndContinue, 0);
    }

    function checkAndContinue() {
        shouldBe('mutations.length', '3');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"style"');
        shouldBe('mutations[0].oldValue', '"color: yellow; width: 100px;"');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"style"');
        shouldBe('mutations[1].oldValue', '"color: red; width: 100px;"');
        shouldBe('mutations[2].type', '"attributes"');
        shouldBe('mutations[2].attributeName', '"style"');
        shouldBe('mutations[2].oldValue', '"color: red; width: 200px;"');

        mutations = null;
        div.getAttribute('style');
        setTimeout(finish, 0);
    }

    function finish() {
        debug('...mutation record created.');

        shouldBe('mutations', 'null');

        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testStyleAttributePropertyAccessIgnoreNoop() {
    var div, path;
    var observer;

    function start() {
        debug('Testing that a no-op style property mutation does not create Mutation Records.');

        mutations = null;
        observer = new MutationObserver(function(m) {
            mutations = m;
        });

        div = document.createElement('div');
        div.setAttribute('style', 'color: yellow; width: 100px;');
        observer.observe(div, { attributes: true });
        div.style.removeProperty('height');

        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations', 'null');

        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testMutateThroughAttrNodeValue() {
    var observer;

    function start() {
        debug('Test that mutating an attribute through an attr node delivers mutation records');

        mutations = null;
        observer = new MutationObserver(function(mutations) {
            window.mutations = mutations;
        });

        div = document.createElement('div');
        div.setAttribute('data-test', 'foo');
        observer.observe(div, { attributes: true, attributeOldValue: true });
        div.attributes['data-test'].value = 'bar';

        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations.length', '1');
        shouldBe('mutations[0].target', 'div');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"data-test"');
        shouldBe('mutations[0].oldValue', '"foo"');

        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testSetAndRemoveAttributeNode() {
    var observer;

    function start() {
        debug('Test that mutating via setAttributeNode delivers mutation records');

        mutations = null;
        observer = new MutationObserver(function(mutations) {
            window.mutations = mutations;
        });

        div = document.createElement('div');
        div.id = 'myId';
        div.setAttribute('data-test', 'foo');
        observer.observe(div, { attributes: true, attributeOldValue: true });
        var attr = document.createAttribute('data-test');
        attr.value = 'bar';
        div.setAttributeNode(attr);
        attr = document.createAttribute('data-other');
        attr.value = 'baz';
        div.setAttributeNode(attr);
        div.removeAttributeNode(div.attributes['id']);

        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations.length', '3');
        shouldBe('mutations[0].target', 'div');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"data-test"');
        shouldBe('mutations[0].oldValue', '"foo"');
        shouldBe('mutations[1].target', 'div');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"data-other"');
        shouldBe('mutations[1].oldValue', 'null');
        shouldBe('mutations[2].target', 'div');
        shouldBe('mutations[2].type', '"attributes"');
        shouldBe('mutations[2].attributeName', '"id"');
        shouldBe('mutations[2].oldValue', '"myId"');

        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testMixedNodeAndElementOperations() {
    var observer;

    function start() {
        debug('Test that setAttribute on an attribute with an existing Attr delivers mutation records');

        mutations = null;
        observer = new MutationObserver(function(mutations) {
            window.mutations = mutations;
        });

        div = document.createElement('div');
        var attr = document.createAttribute('data-test');
        attr.value = 'foo';
        div.setAttributeNode(attr);
        observer.observe(div, { attributes: true, attributeOldValue: true });
        div.setAttribute('data-test', 'bar');

        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations.length', '1');
        shouldBe('mutations[0].target', 'div');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"data-test"');
        shouldBe('mutations[0].oldValue', '"foo"');

        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

function testNamedNodeMapOperations() {
    var observer;

    function start() {
        debug('Test that setNamedItem and removeNamedItem deliver mutation records');

        mutations = null;
        observer = new MutationObserver(function(mutations) {
            window.mutations = mutations;
        });

        div = document.createElement('div');
        div.setAttribute('data-test', 'foo');
        observer.observe(div, { attributes: true, attributeOldValue: true });
        var attr = document.createAttribute('data-test');
        attr.value = 'bar';
        div.attributes.setNamedItem(attr);
        div.attributes.removeNamedItem('data-test');

        setTimeout(finish, 0);
    }

    function finish() {
        shouldBe('mutations.length', '2');
        shouldBe('mutations[0].target', 'div');
        shouldBe('mutations[0].type', '"attributes"');
        shouldBe('mutations[0].attributeName', '"data-test"');
        shouldBe('mutations[0].oldValue', '"foo"');
        shouldBe('mutations[1].target', 'div');
        shouldBe('mutations[1].type', '"attributes"');
        shouldBe('mutations[1].attributeName', '"data-test"');
        shouldBe('mutations[1].oldValue', '"bar"');

        observer.disconnect();
        debug('');
        runNextTest();
    }

    start();
}

var tests = [
    testBasic,
    testWrongType,
    testMultipleRegistration,
    testMultipleObservers,
    testNamespaceURI,
    testPropertyAccess,
    testOrderingWrtDOMSubtreeModified,
    testOldValue,
    testOldValueAsRequested,
    testOldValueUnionMultipleObservations,
    testIDLAttribute,
    testAttributeFilter,
    testAttributeFilterSubtree,
    testAttributeFilterNonHTMLElement,
    testAttributeFilterNonHTMLDocument,
    testStyleAttributePropertyAccess,
    testStyleAttributePropertyAccessOldValue,
    testStyleAttributePropertyAccessIgnoreNoop,
    testMutateThroughAttrNodeValue,
    testSetAndRemoveAttributeNode,
    testMixedNodeAndElementOperations,
    testNamedNodeMapOperations
];
var testIndex = 0;

function runNextTest() {
    if (testIndex < tests.length)
        tests[testIndex++]();
    else
        finishJSTest();
}

description('Test WebKitMutationObserver.observe on attributes');

runNextTest();
</script>
